/******************************************************************************
 * Copyright (c) 2016 TypeFox and others.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 ******************************************************************************/
package org.eclipse.lsp4j.jsonrpc.services;

import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.Set;
import java.util.function.Consumer;

public final class AnnotationUtil {
	private AnnotationUtil() {}

	public static void findDelegateSegments(Class<?> clazz, Set<Class<?>> visited, Consumer<Method> acceptor) {
		if (clazz == null || !visited.add(clazz))
			return;
		findDelegateSegments(clazz.getSuperclass(), visited, acceptor);
		for (Class<?> interf : clazz.getInterfaces()) {
			findDelegateSegments(interf, visited, acceptor);
		}
		for (Method method : clazz.getDeclaredMethods()) {
			if (isDelegateMethod(method)) {
				acceptor.accept(method);
			}
		}
	}

	public static boolean isDelegateMethod(Method method) {
		if (!method.isSynthetic()) {
			JsonDelegate jsonDelegate = method.getAnnotation(JsonDelegate.class);
			if (jsonDelegate != null) {
				if (!(method.getParameterCount() == 0 && method.getReturnType().isInterface())) {
					throw new IllegalStateException(
							"The method " + method.toString() + " is not a proper @JsonDelegate method.");
				}
				return true;
			}
		}
		return false;
	}

	/**
	 * Depth first search for annotated methods in hierarchy.
	 */
	public static void findRpcMethods(Class<?> clazz, Set<Class<?>> visited, Consumer<MethodInfo> acceptor) {
		if (clazz == null || !visited.add(clazz))
			return;
		findRpcMethods(clazz.getSuperclass(), visited, acceptor);
		for (Class<?> interf : clazz.getInterfaces()) {
			findRpcMethods(interf, visited, acceptor);
		}
		String segment = getSegment(clazz);
		for (Method method : clazz.getDeclaredMethods()) {
			MethodInfo methodInfo = createMethodInfo(method, segment);
			if (methodInfo != null) {
				acceptor.accept(methodInfo);
			}
		}
	}

	protected static String getSegment(Class<?> clazz) {
		JsonSegment jsonSegment = clazz.getAnnotation(JsonSegment.class);
		return jsonSegment == null ? "" : jsonSegment.value() + "/";
	}

	protected static MethodInfo createMethodInfo(Method method, String segment) {
		if (!method.isSynthetic()) {
			JsonRequest jsonRequest = method.getAnnotation(JsonRequest.class);
			if (jsonRequest != null) {
				return createRequestInfo(method, segment, jsonRequest);
			}
			JsonNotification jsonNotification = method.getAnnotation(JsonNotification.class);
			if (jsonNotification != null) {
				return createNotificationInfo(method, segment, jsonNotification);
			}
		}
		return null;
	}

	protected static MethodInfo createNotificationInfo(Method method, String segment, JsonNotification jsonNotification) {
		MethodInfo methodInfo = createMethodInfo(method, jsonNotification.useSegment(), segment, jsonNotification.value());
		methodInfo.isNotification = true;
		return methodInfo;
	}

	protected static MethodInfo createRequestInfo(Method method, String segment, JsonRequest jsonRequest) {
		return createMethodInfo(method, jsonRequest.useSegment(), segment, jsonRequest.value());
	}

	protected static MethodInfo createMethodInfo(Method method, boolean useSegment, String segment, String value) {
		method.setAccessible(true);

		final var methodInfo = new MethodInfo();
		methodInfo.method = method;
		methodInfo.parameterTypes = getParameterTypes(method);
		methodInfo.name = getMethodName(method, useSegment, segment, value);
		return methodInfo;
	}

	protected static String getMethodName(Method method, boolean useSegment, String segment, String value) {
		String name = value != null && value.length() > 0 ? value : method.getName();
		return useSegment ? segment + name : name;
	}

	protected static Type[] getParameterTypes(Method method) {
		return Arrays.stream(method.getParameters()).map(Parameter::getParameterizedType).toArray(Type[]::new);
	}

	static class MethodInfo {
		private static Type[] EMPTY_TYPE_ARRAY = {};
		public String name;
		public Method method;
		public Type[] parameterTypes = EMPTY_TYPE_ARRAY;
		public boolean isNotification = false;
	}

	static class DelegateInfo {
		public Method method;
		public Object delegate;
	}
}
